This page provides a comprehensive explanation of the system design, how it works, and the key design decisions made during development.
System Architecture
The system implements a three-stage sensor data pipeline:
Stage 1: Arduino (Sensor Interface) - Reads BNO085 9-axis IMU sensor data via I2C - Converts quaternion and gyroscope data to Euler angles - Packages data into 16-byte SPI packets - Acts as SPI master, transmitting to FPGA
Stage 2: FPGA (Data Bridge) - Receives 16-byte packets from Arduino via SPI slave interface - Parses and buffers sensor data - Acts as SPI slave to MCU, providing data on request - Implements dual SPI slave interfaces (Arduino → FPGA → MCU)
Stage 3: MCU (Data Processing) - Reads sensor data from FPGA via SPI master interface - Processes quaternion and gyroscope data - Implements application-specific logic for sensor data utilization
Data Flow Diagram
BNO085 Sensor (I2C) → Arduino → [SPI] → FPGA → [SPI] → STM32 MCU
The FPGA acts as a bridge, allowing the Arduino (SPI master) and MCU (SPI master) to communicate through the FPGA’s dual slave interfaces.
How the Design Works
SPI Communication Protocol
The system uses SPI Mode 0 (CPOL=0, CPHA=0) with MSB-first bit ordering:
Arduino → FPGA SPI Interface: - Clock Speed: 100kHz - Packet Format: 16 bytes - Byte 0: Header (0xAA) - Bytes 1-2: Roll (int16_t, MSB first, scaled by 100) - Bytes 3-4: Pitch (int16_t, MSB first, scaled by 100) - Bytes 5-6: Yaw (int16_t, MSB first, scaled by 100) - Bytes 7-8: Gyro X (int16_t, MSB first, scaled by 2000) - Bytes 9-10: Gyro Y (int16_t, MSB first, scaled by 2000) - Bytes 11-12: Gyro Z (int16_t, MSB first, scaled by 2000) - Byte 13: Flags (bit 0 = Euler valid, bit 1 = Gyro valid) - Bytes 14-15: Reserved (0x00)
FPGA → MCU SPI Interface: - FPGA passes raw 16-byte packet buffer directly to MCU (same format as received from Arduino) - MCU reads data via SPI master transactions - Packet format: Same 16-byte structure as Arduino sends (header, Roll, Pitch, Yaw, Gyro X/Y/Z, Flags, Reserved) - FPGA acts as transparent bridge, forwarding data without modification
FPGA Implementation
The FPGA implements two SPI slave modules: 1. arduino_spi_slave.sv: Receives data from Arduino - Handles CS-based protocol - Shifts in data on SCK rising edge - Validates header byte (0xAA) - Buffers complete 16-byte packets
spi_slave_mcu.sv: Provides data to MCU- Responds to MCU SPI master requests
- Passes raw 16-byte packet buffer directly to MCU (transparent bridge)
- Implements clock domain crossing from FPGA system clock to MCU SCK domain
- Includes test mode for debugging (outputs known test pattern when enabled)
The top-level module (drum_trigger_top.sv) integrates both SPI slaves and manages data flow between them. The FPGA system clock runs at 3MHz (48MHz HSOSC divided by 16), providing sufficient timing margins for SPI operations (Arduino: 100kHz, MCU: variable).
MCU Implementation
The STM32L432KC MCU: - Configures SPI1 as master (PB3=SCK, PB5=MOSI, PB4=MISO, PA11=NSS) - Reads sensor data from FPGA via SPI transactions - Processes quaternion data for orientation tracking - Uses gyroscope data for motion detection - Implements application-specific processing logic
Key Design Decisions
1. FPGA as Bridge Architecture
Decision: Use FPGA as an intermediate bridge between Arduino and MCU rather than direct Arduino-to-MCU connection.
Rationale: - Allows for data processing and formatting in the FPGA - Provides flexibility for future enhancements (filtering, buffering) - Separates concerns: Arduino handles sensor interface, FPGA handles protocol conversion, MCU handles application logic - Enables independent clock domains and timing control
2. Dual SPI Slave Design
Decision: Implement two independent SPI slave interfaces on the FPGA.
Rationale: - Allows both Arduino and MCU to act as SPI masters - Simplifies protocol design (no arbitration needed) - Enables asynchronous data flow (Arduino can send while MCU reads) - Provides clear separation of data paths
3. 16-Byte Packet Format
Decision: Use fixed 16-byte packets for Arduino-to-FPGA communication.
Rationale: - Fixed size simplifies FPGA buffering logic - Header byte (0xAA) enables packet synchronization - Includes all necessary sensor data (Euler angles + gyroscope) - Leaves room for future expansion (reserved bytes)
4. SPI Mode 0 Selection
Decision: Use SPI Mode 0 (CPOL=0, CPHA=0) for all SPI interfaces.
Rationale: - Standard mode supported by all components - Simplifies timing analysis - Well-documented and widely used - Compatible with Arduino SPI library defaults
Technical Challenges and Solutions
Challenge 1: SPI Timing Synchronization
Problem: Ensuring proper timing between Arduino SPI master and FPGA SPI slave, especially with different clock domains.
Solution: - Used CS-based protocol to clearly define transaction boundaries - Implemented proper clock edge detection (rising edge for data capture) - Added header validation to detect packet start - Implemented state machine to handle SPI transaction lifecycle
Challenge 2: Clock Domain Crossing (CDC)
Problem: Safely transferring data between asynchronous clock domains (Arduino SCK, FPGA system clock, MCU SCK).
Solution: - Implemented CS-based safe read approach: wait for CS high (transaction complete) before reading data - Added 3-cycle delay after CS high to ensure SCK domain is idle (SPI Mode 0 guarantees SCK idle when CS high) - Used atomic reads of complete 16-byte packets to prevent partial updates - Achieved 10:1 timing margin (3 cycles = 1us, well above worst-case SPI timing of 10us) - Documented timing constraints and CDC strategy in TIMING_CONSTRAINTS.md
Challenge 3: Dual SPI Slave Coordination
Problem: Managing two independent SPI slave interfaces without conflicts.
Solution: - Used separate state machines for each SPI slave - Implemented independent buffers for each interface - Added status flags to indicate data availability - Designed clear data flow path: Arduino → Buffer → MCU
Challenge 4: Clock Domain Crossing Between SPI Interfaces
Problem: Transferring data from Arduino SPI slave (clocked on Arduino SCK) to MCU SPI slave (clocked on MCU SCK) through FPGA system clock domain.
Solution: - Used continuous snapshot update approach: snapshot updated when CS is high (transaction inactive) - Snapshot frozen when CS is low (during active transaction) - Data captured from registered clk-domain signals before snapshot - Safe because MCU only reads when CS is low (snapshot is frozen during read) - Verified with comprehensive timing analysis and testbenches
Techniques for Future Students
This project demonstrates several techniques that would be useful for future students:
1. SPI Slave Implementation in Verilog
The FPGA SPI slave modules demonstrate: - Proper handling of SPI clock domain crossing - CS-based transaction management - Bit-level serial data reception - Packet framing and validation
Key Files: arduino_spi_slave.sv, spi_slave_mcu.sv
2. Multi-Master SPI Architecture
The dual slave design shows how to: - Handle multiple SPI masters with one device - Implement independent SPI interfaces - Manage data flow between interfaces - Coordinate timing between different clock domains
Key Files: drum_trigger_top.sv
3. Sensor Data Processing Pipeline
The system demonstrates: - I2C sensor interface (Arduino side) - BNO085 sensor communication - SPI protocol bridge (FPGA transparently forwards data) - Real-time sensor data handling at 100Hz update rate - Clock domain crossing techniques for asynchronous interfaces
Key Files: ARDUINO_SENSOR_BRIDGE.ino, arduino_spi_slave.sv, spi_slave_mcu.sv
4. MCU-FPGA Integration
The MCU code shows: - SPI master configuration for STM32 - Reading structured data from FPGA - Processing quaternion and gyroscope data - Application-level sensor data utilization
Key Files: main.c, STM32L432KC_SPI.c
For detailed implementation details, see the Technical Details page.